昨天最後有提到,假設我們希望某個 route segment 的 children segments 之間,可以共用一個 component,比方說 /dashboard/settings 和 /dashboard/profile 都有一個相同的 header,我們可以使用 App Router 中的兩個特殊檔案 - layout.tsx
和 template.tsx
。
究竟兩者要如何使用?兩者間又有什麼差異呢?讓我們來一探究竟吧!
特殊檔案的副檔名可以接受 .js, .jsx, .tsx
假如希望路由切換時,共用的 components 不會 re-render,達到 persistent layout,可以使用 layout.tsx
來建制 Layouts。
我們在 Day 09 已經簡單介紹過 layout 的用法,還沒讀過文章的讀者可以參考 Day 09 文章。
簡單來說,你在 layout.tsx
中 default export 的 component,可以加入你想共用的 components 和一個 children props,這樣該 route segment 的所有 children segments 都會被傳進 children props 中,舉例來說:
假如我想讓 /dashboard 底下的所有 route segments ( ex: /dashboard/settings , /dashboard/profile, /dashboard/notifications ) 都有一個共用的 <Header>
,我可以在 /dashboard
中建立 layout.tsx
,並將 Header import 進 DashboardLayout 中:
/* /dashboard/layout.tsx */
import React from 'react';
import Header from './Header';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Header />
{children}
</>
);
這樣 /dashboard 的所有子路就都會有 <Header>
。
這邊有個重點,Layout 中不需要 re-render 的 components,在切換路由時不會 re-render,因此會保留 state 和互動的狀態 ( ex: 滾輪位置 )。這也是 layout 和 template 最主要的差異,等介紹完 template 後我們再來做個實驗比較兩者差異。
在使用 layout 時有幾點需要注意:
Next 預設會將 fetch result 存到快取,所以不用擔心重複 fetch 相同 api endpoint 造成效能負擔,細節可參考 Day 28 文章!
page.tsx
和 layout.tsx
可放在同一層資料夾中,page.tsx
也會套用 layout。所以上述例子,/dashboard 的 UI 也會有 Header。Templates 和 Layouts 的概念類似,差別在路由切換時,templates 中所有 components 都會 re-render,因此不會記住當前 state 和互動狀態。
做個小實驗,我們在 Header 中加入兩個功能:
根據上述需求,我寫了一段簡單的程式碼:
<Link>
是用來做路由跳轉;usePathname
是用來取得 URL path,後面文章會提到。
/* app/dashboard/Header.tsx */
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const options = ['Settings', 'Profile', 'Notifications', 'Blog', 'Support'];
export default function Header() {
// 選項會對應到最後一個 route segment
const currentPath = usePathname();
const selectedOption = currentPath.split('/').pop();
return (
<div className='...'>
<div className='...'>
{options.map((option) => (
<Link key={option} href={`/dashboard/${option.toLowerCase()}`}>
<button
key={option}
className={`... ${
option.toLowerCase() === selectedOption &&
'bg-blue-500 text-white'
}`}
>
{option}
</button>
</Link>
))}
</div>
</div>
);
}
假如使用 layout.tsx
,當我們滑到 Header 最右邊,點擊 support 時,頁面會切換到 /dashboard/support
,而且 Header 滾輪依然在最右邊:
但假如我們改成 template.tsx
,裡面內容維持一樣。當滑到 Header 最右邊點擊 support,路由切換後,Header 的滾輪會回到初始位置:
所以 layout 和 template 都能用來讓子路由共享 components,但兩者的差異在於,當路由切換時,layout 不會 re-render,而 template 會 re-render。
上面 sample code 中,我們是用 usePathname
取得 URL path,再從 URL path 取出最後一個 route segment 來判斷目前的選項。今天的最後,想跟大家分享另一個方法 - useSelectedLayoutSegment
。
useSelectedLayoutSegment
是一個用來判斷 layout 底下一層 的 route segments 中,active route segment 是哪個 segment 的 hook。我們一樣來看範例,可能會比較好理解:
上述 <Header>
的 sample code 中,我們是用網址的最後一個 route segment 來判斷哪個選項該是藍底白字。因為我們要的 route segment 剛好在 layout 底下一層,我們也可以用 useSelectedLayoutSegment
來取得 active route segment。比方說 /dashboard/settings 的 active route segment 就會是 settings;/dashbord/profile 的 active route segment 就會是 profile,以此類推。
所以我們也可以讓和 active segment 相同的選項改為藍底白字:
/* app/dashboard/Header.tsx */
'use client';
import Link from 'next/link';
import { useSelectedLayoutSegment } from 'next/navigation';
const options = ['Settings', 'Profile', 'Notifications', 'Blog', 'Support'];
export default function Header() {
// 取得當前的 active segment
const activeSegment = useSelectedLayoutSegment();
return (
<div className='...'>
<div className='...'>
{options.map((option) => (
<Link key={option} href={`/dashboard/${option.toLowerCase()}`}>
<button
key={option}
className={`... ${
option.toLowerCase() === activeSegment &&
'bg-blue-500 text-white'
}`}
>
{option}
</button>
</Link>
))}
</div>
以上就是 layout.tsx 和 template.tsx 的介紹。了解基本路由架構,以及 layout 和 template 怎麼使用後,接下來可能會產生另個疑問:假如我的商品頁網址是 /product/[商品id]
,那我有 100 個商品,我就要在 /product
中建 100 個資料夾嗎?
當然不可能,這時候就可以使用動態路由。這部分就留到明天介紹囉!
謝謝大家耐心的閱讀,我們明天見!